﻿// Copyright (c) Microsoft Corporation
//
// All rights reserved. 
//
// Licensed under the Apache License, Version 2.0 (the ""License""); you may not use this file 
// except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 
// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING 
// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY 
// OR NON-INFRINGEMENT. 
//
// See the Apache Version 2.0 License for specific language governing permissions and limitations under the License.

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using TED.WindowsAzure.Unplugged.Mobile.Exceptions;
using TED.WindowsAzure.Unplugged.Mobile.Sync.File;
using TED.WindowsAzure.Unplugged.Mobile.Sync.Folder;
using Windows.Storage;
using Windows.Storage.Streams;

namespace TED.WindowsAzure.Unplugged.Mobile.Sync.Storage
{
    /// <summary>
    /// Implementation of ISyncFileSTorage using WinRT Storage API for use on Windows 8 and 8.1 applications
    /// </summary>
    public class DefaultSyncFileStorage : ISyncFileStorage
    {
        private readonly ISyncFileStateStorage _stateStorage;

        public DefaultSyncFileStorage()
            : this(new DefaultSyncFileStateStorage())
        {
        }

        public DefaultSyncFileStorage(ISyncFileStateStorage stateStorage)
        {
            _stateStorage = stateStorage;
        }

        public async Task<IRandomAccessStream> OpenReadAsync(ISyncFile instance)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            IStorageFolder localFolder = await GetLocalStorageFolderAsync(instance.FolderName);

            StorageFile file =
                await localFolder.CreateFileAsync(instance.FileName, CreationCollisionOption.OpenIfExists);

            // Give you the stream
            return await file.OpenReadAsync();
        }

        public async Task<IRandomAccessStream> OpenWriteAsync(ISyncFile instance)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            StorageFile file = null;

            IStorageFolder localFolder = await GetLocalStorageFolderAsync(instance.FolderName);

            file = await localFolder.CreateFileAsync(instance.FileName, CreationCollisionOption.ReplaceExisting);

            // need to mark the state of the file as undefined. This will ensure the file is not picked up in any Push routines
            ISyncFileState state = await _stateStorage.LookupAsync(instance.FolderName, instance.FileName);

            if (state == null)
            {
                state = await ConvertToSyncStateFile(instance);
                await _stateStorage.InsertAsync(state);
            }
            else
            {
                state.ApplyPropertiesFrom(instance);
                await _stateStorage.UpdateAsync(state, SyncFileStatus.Undefined);
            }

            instance.Status = SyncFileStatus.Undefined;

            // Give you the stream
            return await file.OpenAsync(FileAccessMode.ReadWrite);
        }

        public async Task<IStorageFile> GetInternalStorageFile(ISyncFile instance)
        {
            StorageFile file = null;

            IStorageFolder localFolder = await GetLocalStorageFolderAsync(instance.FolderName);

            file = await localFolder.CreateFileAsync(instance.FileName, CreationCollisionOption.OpenIfExists);

            return file;
        }

        public async Task DeleteAsync(ISyncFile instance)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            IStorageFolder localFolder = await GetLocalStorageFolderAsync(instance.FolderName);

            try
            {
                // need to mark the state of the file as undefined. This will ensure the file is not picked up in any Push routines
                ISyncFileState state = await _stateStorage.LookupAsync(instance.FolderName, instance.FileName);

                if (state == null)
                {
                    state = await ConvertToSyncStateFile(instance);
                }
                else
                {
                    state.ApplyPropertiesFrom(instance);
                    await _stateStorage.UpdateAsync(state, SyncFileStatus.Undefined);
                }

                // mark the file as deleted - do not remove the record physically from the database
                await _stateStorage.DeleteAsync(state);
            }
            catch (FileNotFoundException)
            {
                // local data storage is out of sync to its file state
                // ignore the exception
            }

            // Give you the actual storage file
            StorageFile file = await localFolder.GetFileAsync(instance.FileName);

            if (file != null)
                await file.DeleteAsync();

            instance.Status = SyncFileStatus.Deleted;
        }

        /// <summary>
        ///     Update a file and mark it ready to sync
        /// </summary>
        /// <param name="instance"></param>
        /// <param name="source"></param>
        /// <returns></returns>
        public async Task InsertAndCommitAsync(ISyncFile instance, IRandomAccessStream source)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            if (source == null)
                throw new ArgumentNullException("source");

            IStorageFolder localFolder = await GetLocalStorageFolderAsync(instance.FolderName);

            // the user has specifically asked to insert, so fail if the file actually exists
            StorageFile file =
                await localFolder.CreateFileAsync(instance.FileName, CreationCollisionOption.FailIfExists);

            // Give you the stream
            IRandomAccessStream target = await file.OpenAsync(FileAccessMode.ReadWrite);

            await RandomAccessStream.CopyAndCloseAsync(source, target);

            ISyncFileState state = await ConvertToSyncStateFile(instance);

            bool stateInsertFailed = false;

            try
            {
                await _stateStorage.InsertAsync(state);
            }
            catch (ArgumentException)
            {
                // the sync state is out of date so ignore
                stateInsertFailed = true;
            }

            if (stateInsertFailed)
                await _stateStorage.UpdateAsync(state, SyncFileStatus.Created);
        }

        /// <summary>
        ///     Marks all files which are undefined as ready to Sync (inserted or updated)
        /// </summary>
        /// <param name="instance"></param>
        /// <param name="source"></param>
        /// <returns></returns>
        public async Task UpdateAndCommitAsync(ISyncFile instance, IRandomAccessStream source)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            if (source == null)
                throw new ArgumentNullException("source");

            IStorageFolder localFolder = await GetLocalStorageFolderAsync(instance.FolderName);

            // the user has specifically asked to insert, so fail if the file actually exists
            StorageFile file =
                await localFolder.CreateFileAsync(instance.FileName, CreationCollisionOption.OpenIfExists);

            // Give you the stream
            IRandomAccessStream target = await file.OpenAsync(FileAccessMode.ReadWrite);

            await RandomAccessStream.CopyAndCloseAsync(source, target);

            // find the state
            ISyncFileState state = await _stateStorage.LookupAsync(instance.FolderName, instance.FileName);

            if (state == null)
                state = await ConvertToSyncStateFile(instance);
            else
            {
                state.ApplyPropertiesFrom(instance);
                state.Status = instance.Status;
            }

            bool stateUpdateFailed = false;

            try
            {
                await _stateStorage.UpdateAsync(state);
            }
            catch (FileNotFoundException)
            {
                // the sync state is out of date so ignore
                stateUpdateFailed = true;
            }

            if (stateUpdateFailed)
                await _stateStorage.InsertAsync(state);
        }

        public async Task<IEnumerable<ISyncFile>> ReadAsync(ISyncFolder folder)
        {
            if (folder == null)
                throw new ArgumentNullException("folder");

            IEnumerable<ISyncFileState> stateFiles = await _stateStorage.ReadAsync(folder.FolderName);

            var localFiles = new List<ISyncFile>();

            foreach (ISyncFileState s in stateFiles)
            {
                localFiles.Add(ConvertFromSyncStateFile(s, folder));
            }

            return localFiles;
        }

        public async Task<ISyncFile> LookupAsync(ISyncFolder folder, int id)
        {
            if (folder == null)
                throw new ArgumentNullException("folder");

            ISyncFile file = null;

            ISyncFileState state = await _stateStorage.LookupAsync(id);

            if (state != null)
                file = ConvertFromSyncStateFile(await _stateStorage.LookupAsync(id), folder);

            return file;
        }

        public async Task<ISyncFile> LookupAsync(ISyncFolder folder, string name)
        {
            if (folder == null)
                throw new ArgumentNullException("folder");

            ISyncFile file = null;

            ISyncFileState state = await _stateStorage.LookupAsync(folder.FolderName, name);

            if (state != null)
                file = ConvertFromSyncStateFile(state, folder);

            return file;
        }

        /// <summary>
        ///     Remove all files (sync and state)
        /// </summary>
        /// <param name="folder"></param>
        /// <returns></returns>
        public async Task TruncateAsync(ISyncFolder folder)
        {
            if (folder == null)
                throw new ArgumentNullException("folder");

            await _stateStorage.TruncateAsync(folder.FolderName);

            IStorageFolder localFolder = await GetLocalStorageFolderAsync(folder.FolderName);

            await localFolder.DeleteAsync(StorageDeleteOption.PermanentDelete);
        }

        public async Task CommitAsync(ISyncFile instance)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            ISyncFileState state = await _stateStorage.LookupAsync(instance.FolderName, instance.FileName);

            if (state != null)
            {
                state.ApplyPropertiesFrom(instance);
                state.Status = instance.Status;
            }

            await CommitStateAsync(instance, state);
        }

        public async Task SyncedAsync(ISyncFile instance, SyncFileStatus status)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            ISyncFileState state = await _stateStorage.LookupAsync(instance.FolderName, instance.FileName);

            if (state != null)
            {
                state.ApplyPropertiesFrom(instance);

                if (status == SyncFileStatus.Deleted)
                {
                    try
                    {
                        await _stateStorage.DeleteAsync(state, DeleteMode.Hard);
                    }
                    catch (FileNotFoundException)
                    {
                        // continue as what looks like has happened is the underlying state is not matched to the sync folder
                    }
                }
                else
                {
                    await _stateStorage.UpdateAsync(state, status);
                }
            }

            instance.Status = SyncFileStatus.AsServer;
        }

        public async Task FlushAsync(ISyncFolder folder)
        {
            if (folder == null)
                throw new ArgumentNullException("folder");

            IEnumerable<ISyncFileState> results =
                await _stateStorage.ReadAsync(folder.FolderName, SyncFileStatus.Undefined);

            foreach (ISyncFileState state in results)
            {
                ISyncFile instance = ConvertFromSyncStateFile(state, folder);
                await CommitStateAsync(instance, state);
            }
        }

        /// <summary>
        ///     Creates a new file in the folder
        /// </summary>
        /// <param name="name"></param>
        /// <param name="folder"></param>
        /// <returns></returns>
        public async Task<ISyncFile> CreateFileAsync(string name, ISyncFolder folder)
        {
            if (folder == null)
                throw new ArgumentNullException("folder");

            ISyncFile file = null;

            // first of all determine if the file already exists by looking at the state database
            ISyncFileState state = await _stateStorage.LookupAsync(folder.FolderName, name);

            if (state != null)
            {
                // existing entry therefore the status should be set to updated
                if (state.Status != SyncFileStatus.Undefined)
                {
                    string message = "A file with a similar name already exists in the sync container.";

                    if (state.Status == SyncFileStatus.Deleted)
                    {
                        // a delete has happened but the push has not
                        // the user must decide if the data info should be removed 
                        // or a push should happen first
                        message = "A file has been marked as deleted but a push to the server has not happened yet.";
                    }

                    throw new Exception(message);
                }

                // new file so create new state
                file = new SyncFile(folder) {FileName = name};
                file.ApplyPropertiesFrom(state);
            }
            else
            {
                // new file so create new state
                file = new SyncFile(folder) {FileName = name};

                state = await _stateStorage.CreateFileAsync();
                state.ApplyPropertiesFrom(file);
            }

            // finally set the status of the file to 
            file.Status = SyncFileStatus.Undefined;
            await _stateStorage.OpenWriteAsync(state);

            return file;
        }

        public async Task CancelAsync(ISyncFolder folder)
        {
            if (folder == null)
                throw new ArgumentNullException("folder");

            await _stateStorage.CancelAsync(folder.FolderName);
        }

        public async Task UndoAsync(ISyncFile instance)
        {
            if (instance == null)
                throw new ArgumentNullException("instance");

            ISyncFileState state = await _stateStorage.LookupAsync(instance.FolderName, instance.FileName);

            if (state.Status == SyncFileStatus.Undefined)
            {
                if (state != null)
                    await _stateStorage.UndoAsync(state);

                state = await _stateStorage.LookupAsync(instance.FolderName, instance.FileName);

                if (state != null)
                    instance.Status = state.Status;
            }
        }

        private async Task CommitStateAsync(ISyncFile instance, ISyncFileState state)
        {
            // check to see if the file exists in the sync folder.
            // if it doesn't then the file has been deleted and therefore the state file should be deleted as well
            IStorageFile file = await FetchFileAsync(instance, instance.FolderName);

            if (file == null)
            {
                // if the previous state was undefined then the user has called CreateFile and then Committed but has not
                // saved a disk to file. In this case we should assume that it is an error on the users behalf and 
                // inform them appropriately.
                if (state != null)
                {
                    // if the previous state is undefined throw error
                    if (state.PreviousStatus == SyncFileStatus.Undefined)
                        throw new SyncException(string.Format("You must save file {0} to disk before saving, or undo",
                            instance.FileName));

                    // the file no longer exists so we can assume it has been deleted
                }
                else
                {
                    // assume the file has been removed from disk intentionally
                    await _stateStorage.DeleteAsync(state);
                }
            }
            else
            {
                // the file exists so its state needs to updated to Created or Updated
                if (state == null)
                {
                    state = await ConvertToSyncStateFile(instance);
                    await _stateStorage.InsertAsync(state);
                    instance.Status = SyncFileStatus.Created;
                }
                else
                {
                    instance.Status = state.PreviousStatus == SyncFileStatus.AsServer
                        ? SyncFileStatus.Updated
                        : SyncFileStatus.Created;

                    // the file exists therefore its state needs to be set to Updated
                    await _stateStorage.UpdateAsync(state, instance.Status);
                }
            }
        }

        private async Task<IStorageFile> FetchFileAsync(ISyncFile instance, string folderName)
        {
            IStorageFolder localFolder = await GetLocalStorageFolderAsync(folderName);

            try
            {
                StorageFile file = await localFolder.GetFileAsync(instance.FileName);
                return file;
            }
            catch (FileNotFoundException)
            {
                return null;
            }
        }

        private async Task<IStorageFolder> GetLocalStorageFolderAsync(string folderName)
        {
            return
                await
                    ApplicationData.Current.LocalFolder.CreateFolderAsync(folderName,
                        CreationCollisionOption.OpenIfExists);
        }

        private async Task<ISyncFileState> ConvertToSyncStateFile(ISyncFile file)
        {
            ISyncFileState state = await _stateStorage.CreateFileAsync();

            state.ApplyPropertiesFrom(file);

            return state;
        }

        private ISyncFile ConvertFromSyncStateFile(ISyncFileState state, ISyncFolder folder)
        {
            var file = new SyncFile(folder);

            file.ApplyPropertiesFrom(state);

            return file;
        }
    }
}